﻿using System;
using System.ComponentModel;
using System.Data;
using System.Text.RegularExpressions;
using System.Web;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using BlogEngine.Core.Web.Extensions;

/// <summary>
/// The <see cref="LinkFixer"/>
///   class is a BlogEngine.Net extension that identifies fully qualified uri address that can be made relative to the blog when saving or serving
///   a post, page or comment.
/// </summary>
[Extension(
    "Modifies post contents to fix resource addressing between http and https and converts alias domains and the publishing domain to relative addresses."
    , "1.0", "<a href=\"http://neovolve.com\">Rory Primrose</a>")]
public class LinkFixer
{
    /// <summary>
    /// Defines the setting key used to identify whether entries should be converted when they are saving.
    /// </summary>
    private const String ConvertOnSavingKey = "ConvertOnSaving";

    /// <summary>
    /// Defines the setting key used to identify whether entries should be converted when they are served to a client.
    /// </summary>
    private const String ConvertOnServingKey = "ConvertOnServing";

    /// <summary>
    /// Defines the setting key used to identify whether https links should be downgraded to http where appropriate.
    /// </summary>
    private const String ConvertToHttpKey = "ConvertToHTTP";

    /// <summary>
    /// Defines the setting key used to identify whether http links should be upgraded to https where appropriate.
    /// </summary>
    private const String ConvertToHttpsKey = "ConvertToHTTPS";

    /// <summary>
    /// Defines the setting key used to identify domain alias values.
    /// </summary>
    private const String DomainAliasKey = "DomainAlias";

    /// <summary>
    /// Stores the domain settings for the extension.
    /// </summary>
    private static readonly ExtensionSettings _domainSettings;

    /// <summary>
    /// Defines the name of the extension.
    /// </summary>
    private static readonly String _extensionName = typeof(LinkFixer).Name;

    /// <summary>
    /// Defines the resource links regular expression.
    /// </summary>
    private static readonly Regex _resourceExpression = new Regex(
        "<[^>]+(src|href)\\s*=\\s*[\"']?(?<Address>[^\"'\\s>]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>
    /// Stores the settings for the extension.
    /// </summary>
    private static readonly ExtensionSettings _settings;

    /// <summary>
    /// Initializes static members of the <see cref="LinkFixer"/> class.
    /// </summary>
    static LinkFixer()
    {
        Page.Serving += PostServering;
        Post.Serving += PostServering;
        Comment.Serving += PostServering;
        Page.Saving += PageSaving;
        Post.Saving += PostSaving;
        Post.AddingComment += CommentSaving;

        // Load the settings for the extension
        _settings = LoadSettings();
        _domainSettings = LoadDomainSettings();
    }

    /// <summary>
    /// Comments the saving.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="e">
    /// The <see cref="System.ComponentModel.CancelEventArgs"/> instance containing the event data.
    /// </param>
    private static void CommentSaving(Object sender, CancelEventArgs e)
    {
        Comment comment = sender as Comment;

        if (comment == null)
        {
            return;
        }

        if (ShouldConvertOnSaving)
        {
            // Update the body of this comment
            comment.Content = UpdatePostContent(comment.Content);
        }
    }

    /// <summary>
    /// Converts the URL.
    /// </summary>
    /// <param name="url">
    /// The URL to convert.
    /// </param>
    /// <param name="domains">
    /// The domains.
    /// </param>
    /// <returns>
    /// A <see cref="String"/> instance.
    /// </returns>
    private static String ConvertUrl(String url, DataTable domains)
    {
        Uri parsedUri;

        if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out parsedUri) == false)
        {
            return url;
        }

        // We only need to process absolute urls
        if (parsedUri.IsAbsoluteUri == false)
        {
            return url;
        }

        Uri servingAddress = HttpContext.Current.Request.Url;

        foreach (DataRow row in domains.Rows)
        {
            String domainAlias = row[DomainAliasKey].ToString();

            if (domainAlias.StartsWith("*."))
            {
                // We are going to check for a wildcard domain name
                String domainToFind = domainAlias.Substring(2);

                if (parsedUri.Host.EndsWith(domainToFind, StringComparison.OrdinalIgnoreCase) == false)
                {
                    // The host of this url does not match the wildcard domain
                    continue;
                }
            }
            else
            {
                // We are going to check the full host name
                if (parsedUri.Host.Equals(domainAlias, StringComparison.OrdinalIgnoreCase) == false)
                {
                    continue;
                }
            }

            return MakeUriRelative(parsedUri, servingAddress);
        }

        // Process the current host as an alias domain
        // We are going to check the full host name for this scenario
        if (parsedUri.Host.Equals(servingAddress.Host, StringComparison.OrdinalIgnoreCase))
        {
            return MakeUriRelative(parsedUri, servingAddress);
        }

        return url;
    }

    /// <summary>
    /// Loads the domain settings.
    /// </summary>
    /// <returns>
    /// A <see cref="ExtensionSettings"/> instance.
    /// </returns>
    private static ExtensionSettings LoadDomainSettings()
    {
        ExtensionSettings defaultSettings = new ExtensionSettings
        {
            Name = "Domains",
            IsScalar = false
        };

        // Define the structure for the settings
        defaultSettings.AddParameter(DomainAliasKey, "Domain alias", 256, true, true);

        defaultSettings.Help =
            @"<b>Domain Alias:</b> Any domain name that maps to this blog. 
Links found that match the domain alias will be cleaned up to match the serving address.
Domain alias may be a specific domain with a host header (www.neovolve.com or blog.neovolve.com), 
without a host header (neovolve.com) 
or a wildcard domain (*.neovolve.com).<br/>";

        // Add the current host as an alias domain
        String currentHost = String.Empty;

        try
        {
            currentHost = HttpContext.Current.Request.Url.Host;
        }
        catch (HttpException)
        {
            // There might not be a current request happening (applicatin initialization for example)
        }

        const String LocalHostName = "localhost";

        // Add a default record
        defaultSettings.AddValue(DomainAliasKey, LocalHostName);

        if (String.IsNullOrEmpty(currentHost) == false && currentHost.Equals(LocalHostName, StringComparison.OrdinalIgnoreCase) == false)
        {
            defaultSettings.AddValue(DomainAliasKey, currentHost);
        }

        // Initialize the settings
        return ExtensionManager.InitSettings(_extensionName, defaultSettings);
    }

    /// <summary>
    /// Loads the settings.
    /// </summary>
    /// <returns>
    /// A <see cref="ExtensionSettings"/> instance.
    /// </returns>
    private static ExtensionSettings LoadSettings()
    {
        ExtensionSettings defaultSettings = new ExtensionSettings
        {
            Name = "Settings",
            IsScalar = true
        };

        // Define the structure for the settings
        defaultSettings.AddParameter(ConvertOnSavingKey, "Convert on saving", 5, true, false, ParameterType.Boolean);
        defaultSettings.AddParameter(ConvertOnServingKey, "Convert on serving", 5, true, false, ParameterType.Boolean);
        defaultSettings.AddParameter(ConvertToHttpsKey, "Convert to HTTPS", 5, true, false, ParameterType.Boolean);
        defaultSettings.AddParameter(ConvertToHttpKey, "Convert to HTTP", 5, true, false, ParameterType.Boolean);

        defaultSettings.Help =
            @"<b>Convert to HTTPS:</b> Allows HTTPS address to be downgraded to HTTP where domain alias or current host matches link.<br/>
<b>Convert to HTTP:</b> Allows HTTP address to be upgraded to HTTPS where domain alias or current host matches link.<br/>
<b>Convert on saving:</b> Converts entry contents when saving the entry.<br/>
<b>Convert on serving:</b> Converts entry contents when serving the entry to a client. 
Disabling this entry will increase site performance but ensure existing entries do not use fully qualified addresses.<br/>";

        // Add the default values
        defaultSettings.AddValue(ConvertToHttpsKey, true);
        defaultSettings.AddValue(ConvertToHttpKey, false);
        defaultSettings.AddValue(ConvertOnSavingKey, true);
        defaultSettings.AddValue(ConvertOnServingKey, true);

        // Initialize the settings
        return ExtensionManager.InitSettings(_extensionName, defaultSettings);
    }

    /// <summary>
    /// Makes the URI relative.
    /// </summary>
    /// <param name="address">
    /// The address.
    /// </param>
    /// <param name="servingAddress">
    /// The serving address.
    /// </param>
    /// <returns>
    /// A <see cref="String"/> instance.
    /// </returns>
    private static String MakeUriRelative(Uri address, Uri servingAddress)
    {
        if (address.Scheme.Equals(servingAddress.Scheme, StringComparison.OrdinalIgnoreCase))
        {
            // Make the address relative
            return address.PathAndQuery;
        }

        if (address.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
        {
            // Check if we can convert from https to http
            if (CanConvertToHttp)
            {
                return address.PathAndQuery;
            }
        }
        else if (address.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
        {
            // Check if we can convert from http to https
            if (CanConvertToHttps)
            {
                return address.PathAndQuery;
            }
        }

        // The address has an unknown scheme or a scheme that cannot be changed
        // The full uri will be retaineed
        return address.ToString();
    }

    /// <summary>
    /// Pages the saving.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="e">
    /// The <see cref="BlogEngine.Core.SavedEventArgs"/> instance containing the event data.
    /// </param>
    private static void PageSaving(Object sender, SavedEventArgs e)
    {
        Page page = sender as Page;

        if (page == null)
        {
            return;
        }

        if (ShouldConvertOnSaving)
        {
            page.Content = UpdatePostContent(page.Content);
        }
    }

    /// <summary>
    /// Posts the saving.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="e">
    /// The <see cref="BlogEngine.Core.SavedEventArgs"/> instance containing the event data.
    /// </param>
    private static void PostSaving(Object sender, SavedEventArgs e)
    {
        Post post = sender as Post;

        if (post == null)
        {
            return;
        }

        if (ShouldConvertOnSaving)
        {
            post.Content = UpdatePostContent(post.Content);
        }
    }

    /// <summary>
    /// Posts the servering.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="e">
    /// The <see cref="BlogEngine.Core.ServingEventArgs"/> instance containing the event data.
    /// </param>
    private static void PostServering(Object sender, ServingEventArgs e)
    {
        if (ShouldConvertOnServing)
        {
            e.Body = UpdatePostContent(e.Body);
        }
    }

    /// <summary>
    /// Updates the content of the post.
    /// </summary>
    /// <param name="content">
    /// The content.
    /// </param>
    /// <returns>
    /// A <see cref="String"/> instance.
    /// </returns>
    private static String UpdatePostContent(String content)
    {
        MatchCollection resourceLinks = _resourceExpression.Matches(content);
        DataTable domains = _domainSettings.GetDataTable();

        for (Int32 index = resourceLinks.Count - 1; index >= 0; index--)
        {
            // Convert this match
            Capture resourceLink = resourceLinks[index].Groups["Address"];
            String url = resourceLink.Value;
            String newUrl = ConvertUrl(url, domains);

            if (url != newUrl)
            {
                // Replace the url in the body
                content = String.Concat(content.Substring(0, resourceLink.Index), newUrl, content.Substring(resourceLink.Index + resourceLink.Length));
            }
        }

        return content;
    }

    /// <summary>
    /// Gets a value indicating whether this instance can convert to HTTP.
    /// </summary>
    /// <value>
    /// <c>true</c> if this instance can convert to HTTP; otherwise, <c>false</c>.
    /// </value>
    private static Boolean CanConvertToHttp
    {
        get
        {
            String settingValue = _settings.GetSingleValue(ConvertToHttpKey);

            if (String.IsNullOrEmpty(settingValue))
            {
                return false;
            }

            Boolean canConvert;

            if (Boolean.TryParse(settingValue, out canConvert))
            {
                return canConvert;
            }

            return false;
        }
    }

    /// <summary>
    /// Gets a value indicating whether this instance can convert to HTTPS.
    /// </summary>
    /// <value>
    /// <c>true</c> if this instance can convert to HTTPS; otherwise, <c>false</c>.
    /// </value>
    private static Boolean CanConvertToHttps
    {
        get
        {
            String settingValue = _settings.GetSingleValue(ConvertToHttpsKey);

            if (String.IsNullOrEmpty(settingValue))
            {
                return true;
            }

            Boolean canConvert;

            if (Boolean.TryParse(settingValue, out canConvert))
            {
                return canConvert;
            }

            return true;
        }
    }

    /// <summary>
    /// Gets a value indicating whether post entries should be converted when saving.
    /// </summary>
    /// <value>
    /// <c>true</c> if post entries should be converted when saving; otherwise, <c>false</c>.
    /// </value>
    private static Boolean ShouldConvertOnSaving
    {
        get
        {
            String settingValue = _settings.GetSingleValue(ConvertOnSavingKey);

            if (String.IsNullOrEmpty(settingValue))
            {
                return true;
            }

            Boolean shouldConvert;

            if (Boolean.TryParse(settingValue, out shouldConvert))
            {
                return shouldConvert;
            }

            return true;
        }
    }

    /// <summary>
    /// Gets a value indicating whether post entries should be converted when serving.
    /// </summary>
    /// <value>
    /// <c>true</c> if post entries should be converted when serving; otherwise, <c>false</c>.
    /// </value>
    private static Boolean ShouldConvertOnServing
    {
        get
        {
            String settingValue = _settings.GetSingleValue(ConvertOnServingKey);

            if (String.IsNullOrEmpty(settingValue))
            {
                return true;
            }

            Boolean shouldConvert;

            if (Boolean.TryParse(settingValue, out shouldConvert))
            {
                return shouldConvert;
            }

            return true;
        }
    }
}